/*
 * This file is part of Cadmium.
 * Copyright (C) 2007-2010 Xavier Clerc.
 *
 * Cadmium is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Cadmium is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package fr.x9c.cadmium.kernel;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import fr.x9c.cadmium.util.ByteCode;
import fr.x9c.cadmium.util.CustomClassLoader;

/**
 * This class implements a method that compiles a dispatcher into Java bytecode
 * to speed up execution.
 *
 * @author <a href="mailto:cadmium@x9c.fr">Xavier Clerc</a>
 * @version 1.0
 * @since 1.0
 */
final class DispatcherCompiler {

    /** Prefix for class names. */
    private static final String CLASS_NAME_PREFIX =
        "fr.x9c.cadmium.kernel.CompiledDispatcher";

    /** Next integer identifier for class name. */
    private static final AtomicInteger NEXT_CLASS_NAME_ID =
        new AtomicInteger();

    /**
     * No instance of this class.
     */
    private DispatcherCompiler() {
    } // end empty constructor

    /**
     * Compiles and returns a dispatcher. <br/>
     * Primitives subsequently added to the returned dispatcher are handled
     * by an embedded {@link fr.x9c.cadmium.kernel.ReflectionDispatcher}
     * @param names primitive names - should not be <tt>null</tt><br/>
     *              should have the same length as <tt>impl</tt>
     * @param impls primitive implementations - should not be <tt>null</tt><br/>
     *              should have the same length as <tt>names</tt><br/>
     *              should not be empty
     * @return a compiled dispatcher for passed primitives, <tt>null</tt>
     *         if an error occurs while trying to load the compiled dispatcher
     *         into the JVM <br/>
     *         the dispatcher is a descendant of {@link fr.x9c.cadmium.kernel.DispatcherCompiler.AbstractCompiledDispatcher}
     */
    static Dispatcher make(final String[] names, final Method[] impls) {
        assert names != null : "null names";
        assert impls != null : "null impls";
        assert names.length == impls.length : "names and impls should have the same length";
        assert impls.length > 0 : "impls should not be empty";
        try {
            final String className =
                DispatcherCompiler.CLASS_NAME_PREFIX
                + DispatcherCompiler.NEXT_CLASS_NAME_ID.incrementAndGet();
            final byte[] bytecode = createBytecode(className, impls);
            final Class cl =
                CustomClassLoader.INSTANCE.defineAndResolve(className, bytecode);
            final AbstractCompiledDispatcher res =
                (AbstractCompiledDispatcher) cl.newInstance();
            res.disp = new ReflectionDispatcher(names, impls);
            res.compiled = names.length;
            return res;
        } catch (final Throwable t) {
            return null;
        } // end try/catch
    } // end method 'make(String[], Method[])'

    /**
     * Creates and returns the bytecode for a dispatcher. <br/>
     * The class whose code is returned is a descendant of {@link fr.x9c.cadmium.kernel.DispatcherCompiler.AbstractCompiledDispatcher}.
     * @param name class name - should not be <tt>null</tt>
     * @param impls methods to dispatch - should not be <tt>null</tt>
     * @return the bytecode for the requested dispatcher
     * @throws IOException never, as i/o is not over a
     *                     {@link java.io.ByteArrayOutputStream}
     */
    private static byte[] createBytecode(final String name,
                                         final Method[] impls)
        throws IOException {
        assert name != null : "null name";
        assert impls != null : "null impls";
        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
        final DataOutputStream out = new DataOutputStream(baos);

        final int nbImpls = impls.length;
        // map from arity to signatures in constant pool
        final Map<Integer, Integer> signatures =
            new LinkedHashMap<Integer, Integer>();
        // map from class to class name in constant pool
        final Map<Class, Integer> classNames =
            new LinkedHashMap<Class, Integer>();
        // method arities
        final int[] arities = new int[nbImpls];
        int maxArity = -1;
        for (int i = 0; i < nbImpls; i++) {
            final Method m = impls[i];
            final int arity = m.getParameterTypes().length;
            signatures.put(arity, null);
            classNames.put(m.getDeclaringClass(), null);
            arities[i] = arity;
            if (arity > maxArity) {
                maxArity = arity;
            } // end if
        } // end for

        // header
        out.writeInt(ByteCode.FILE_MAGIC);
        out.writeShort(ByteCode.MINOR_VERSION);
        out.writeShort(ByteCode.MAJOR_VERSION);

        // constant_pool count (actual_size + 1)
        out.writeShort(18 + 2 * classNames.size() + signatures.size() + 3 * nbImpls + 1);

        // 1: this [CONSTANT_Class_info]
        out.writeByte(ByteCode.CONSTANT_CLASS);
        out.writeShort(2);

        // 2: class name [CONSTANT_Utf8_info]
        out.writeByte(ByteCode.CONSTANT_UTF8);
        final byte[] utf8ClassName = ByteCode.modifiedUTF8(name.replace(".", "/"));
        out.writeShort(utf8ClassName.length);
        out.write(utf8ClassName);

        // 3: super [CONSTANT_Class_info]
        out.writeByte(ByteCode.CONSTANT_CLASS);
        out.writeShort(4);

        // 4: super class name [CONSTANT_Utf8_info]
        out.writeByte(ByteCode.CONSTANT_UTF8);
        final byte[] utf8SuperName =
            ByteCode.modifiedUTF8(AbstractCompiledDispatcher.class.getName().replace(".", "/"));
        out.writeShort(utf8SuperName.length);
        out.write(utf8SuperName);

        // 5: empty constructor name [CONSTANT_Utf8_info]
        out.writeByte(ByteCode.CONSTANT_UTF8);
        final byte[] utf8EmptyConstructorName = ByteCode.modifiedUTF8("<init>");
        out.writeShort(utf8EmptyConstructorName.length);
        out.write(utf8EmptyConstructorName);

        // 6: empty constructor descriptor [CONSTANT_Utf8_info]
        out.writeByte(ByteCode.CONSTANT_UTF8);
        final byte[] utf8EmptyConstructorDesc = ByteCode.modifiedUTF8("()V");
        out.writeShort(utf8EmptyConstructorDesc.length);
        out.write(utf8EmptyConstructorDesc);

        // 7: "Code" [CONSTANT_Utf8_info]
        out.writeByte(ByteCode.CONSTANT_UTF8);
        final byte[] utf8Code = ByteCode.modifiedUTF8("Code");
        out.writeShort(utf8Code.length);
        out.write(utf8Code);

        // 8: reference to AbstractCompiledDispatcher empty constructor
        out.writeByte(ByteCode.CONSTANT_METHOD_REF);
        out.writeShort(3);
        out.writeShort(9);

        // 9: name and type of AbstractCompiledDispatcher empty constructor
        out.writeByte(ByteCode.CONSTANT_NAME_AND_TYPE);
        out.writeShort(5);
        out.writeShort(6);

        // 10: 'Value call(int, CodeRunner, Value[])' name [CONSTANT_Utf8_info]
        out.writeByte(ByteCode.CONSTANT_UTF8);
        final byte[] utf8CallName = ByteCode.modifiedUTF8("call");
        out.writeShort(utf8CallName.length);
        out.write(utf8CallName);

        // 11: 'Value call(int, CodeRunner, Value[])' descriptor [CONSTANT_Utf8_info]
        out.writeByte(ByteCode.CONSTANT_UTF8);
        final byte[] utf8CallDesc = ByteCode.modifiedUTF8("(ILfr/x9c/cadmium/kernel/CodeRunner;[Lfr/x9c/cadmium/kernel/Value;)Lfr/x9c/cadmium/kernel/Value;");
        out.writeShort(utf8CallDesc.length);
        out.write(utf8CallDesc);

        // 12: "Exceptions" [CONSTANT_Utf8_info]
        out.writeByte(ByteCode.CONSTANT_UTF8);
        final byte[] utf8Exceptions = ByteCode.modifiedUTF8("Exceptions");
        out.writeShort(utf8Exceptions.length);
        out.write(utf8Exceptions);

        // 13:  [CONSTANT_Class_info]
        out.writeByte(ByteCode.CONSTANT_CLASS);
        out.writeShort(14);

        // 14:  [CONSTANT_Utf8_info]
        out.writeByte(ByteCode.CONSTANT_UTF8);
        final byte[] utf8Fail = ByteCode.modifiedUTF8(Fail.Exception.class.getName().replace(".", "/"));
        out.writeShort(utf8Fail.length);
        out.write(utf8Fail);

        // 15:  [CONSTANT_Class_info]
        out.writeByte(ByteCode.CONSTANT_CLASS);
        out.writeShort(16);

        // 16:  [CONSTANT_Utf8_info]
        out.writeByte(ByteCode.CONSTANT_UTF8);
        final byte[] utf8Fatal = ByteCode.modifiedUTF8(Fatal.Exception.class.getName().replace(".", "/"));
        out.writeShort(utf8Fatal.length);
        out.write(utf8Fatal);

        // 17:  [CONSTANT_Class_info]
        out.writeByte(ByteCode.CONSTANT_CLASS);
        out.writeShort(18);

        // 18:  [CONSTANT_Utf8_info]
        out.writeByte(ByteCode.CONSTANT_UTF8);
        final byte[] utf8FalseExit = ByteCode.modifiedUTF8(FalseExit.class.getName().replace(".", "/"));
        out.writeShort(utf8FalseExit.length);
        out.write(utf8FalseExit);

        int cpool = 19;

        // classes
        for (Map.Entry<Class, Integer> e : classNames.entrySet()) {
            final Class cl = e.getKey();
            e.setValue(cpool);
            // cpool
            out.writeByte(ByteCode.CONSTANT_CLASS);
            out.writeShort(cpool + 1);
            // cpool + 1
            out.writeByte(ByteCode.CONSTANT_UTF8);
            final byte[] utf8 = ByteCode.modifiedUTF8(cl.getName().replace(".", "/"));
            out.writeShort(utf8.length);
            out.write(utf8);
            cpool += 2;
        } // end for

        // method signatures
        for (Map.Entry<Integer, Integer> e : signatures.entrySet()) {
            final int a = e.getKey();
            e.setValue(cpool++);
            out.writeByte(ByteCode.CONSTANT_UTF8);
            final StringBuilder sb = new StringBuilder("(Lfr/x9c/cadmium/kernel/CodeRunner;");
            for (int i = 1; i < a; i++) {
                sb.append("Lfr/x9c/cadmium/kernel/Value;");
            } // end for
            sb.append(")Lfr/x9c/cadmium/kernel/Value;");
            final byte[] utf8 = ByteCode.modifiedUTF8(sb.toString());
            out.writeShort(utf8.length);
            out.write(utf8);
        } // end for

        // method references
        final int[] methodRef = new int[nbImpls];
        for (int i = 0; i < nbImpls; i++) {
            final Method m = impls[i];
            // cpool
            out.writeByte(ByteCode.CONSTANT_METHOD_REF);
            out.writeShort(classNames.get(m.getDeclaringClass()));
            out.writeShort(cpool + 1);

            // cpool + 1
            out.writeByte(ByteCode.CONSTANT_NAME_AND_TYPE);
            out.writeShort(cpool + 2);
            out.writeShort(signatures.get(m.getParameterTypes().length));

            // cpool + 2
            out.writeByte(ByteCode.CONSTANT_UTF8);
            final byte[] utf8 = ByteCode.modifiedUTF8(m.getName());
            out.writeShort(utf8.length);
            out.write(utf8);

            methodRef[i] = cpool;
            cpool += 3;
        } // end for

        if (cpool > (int) Short.MAX_VALUE) {
            return null;
        } // end if

        final int[] switchOffset = new int[nbImpls];
        int codeLength = 16 // aload_1 and tableswitch (with padding)
            + (nbImpls * 4); // jump offsets
        for (int i = 0; i < nbImpls; i++) {
            final Method m = impls[i];
            switchOffset[i] = codeLength - 1;
            final int nbValues = arities[i] - 1;
            final int nbValues0TO5 = Math.min(nbValues, 6);
            final int nbValuesLeft = nbValues - nbValues0TO5;
            codeLength += 5 + nbValues0TO5 * 3 + nbValuesLeft * 4;
        } // end for
        final int defaultOffset = codeLength - 1;
        codeLength += 2; // default case


        // class definition
        out.writeShort(ByteCode.ACC_PUBLIC
                       | ByteCode.ACC_FINAL
                       | ByteCode.ACC_SYNTHETIC
                       | ByteCode.ACC_SUPER); // access_flags
        out.writeShort(1); // this_class
        out.writeShort(3); // super_class
        out.writeShort(0); // interfaces_count
        out.writeShort(0); // fields_count
        out.writeShort(2); // methods_count

        // 'empty constructor' method
        out.writeShort(ByteCode.ACC_PUBLIC
                       | ByteCode.ACC_SYNTHETIC); // access_flags
        out.writeShort(5); // name_index
        out.writeShort(6); // descriptor_index
        out.writeShort(1); // attributes_count
        out.writeShort(7); // code attribute
        out.writeInt(17); // attribute length
        out.writeShort(1); // max stack
        out.writeShort(1); // max_locals
        out.writeInt(5); // code_length
        out.writeByte(ByteCode.INSTR_ALOAD_0);
        out.writeByte(ByteCode.INSTR_INVOKESPECIAL);
        out.writeShort(8);
        out.writeByte(ByteCode.INSTR_RETURN);
        out.writeShort(0); // exception_table_length
        out.writeShort(0); // attributes_count

        // 'Value call(int, CodeRunner, Value[])' method
        out.writeShort(ByteCode.ACC_PROTECTED
                       | ByteCode.ACC_FINAL
                       | ByteCode.ACC_SYNTHETIC); // access_flags
        out.writeShort(10); // name_index
        out.writeShort(11); // descriptor_index
        out.writeShort(2); // attributes_count
        out.writeShort(12); // exceptions attribute
        out.writeInt(8); // attribute length
        out.writeShort(3); // number_of_exceptions
        out.writeShort(13); // Fail.Exception
        out.writeShort(15); // Fatal.Exception
        out.writeShort(17); // FalseExit
        out.writeShort(7); // code attribute
        out.writeInt(12 + codeLength); // attribute length
        out.writeShort(maxArity + 2); // max stack
        out.writeShort(4); // max_locals
        out.writeInt(codeLength);
        out.writeByte(ByteCode.INSTR_ILOAD_1);
        out.writeByte(ByteCode.INSTR_TABLESWITCH);
        out.writeShort(0); // padding
        out.writeInt(defaultOffset); // default
        out.writeInt(0); // low
        out.writeInt(nbImpls - 1); // high

        for (int i = 0; i < nbImpls; i++) { // jump offsets
            out.writeInt(switchOffset[i]);
        } // end for

        for (int i = 0; i < nbImpls; i++) { // each case
            final Method m = impls[i];
            final int a = arities[i];
            out.writeByte(ByteCode.INSTR_ALOAD_2); // push context
            for (int j = 1; j < a; j++) { // push arguments
                out.writeByte(ByteCode.INSTR_ALOAD_3);
                final int cste = j - 1;
                if (cste <= 5) {
                    out.writeByte(ByteCode.INSTR_ICONST_0 + cste);
                } else {
                    out.writeByte(ByteCode.INSTR_BIPUSH);
                    out.writeByte(cste);
                } // end if/else
                out.writeByte(ByteCode.INSTR_AALOAD);
            } // end for
            out.writeByte(ByteCode.INSTR_INVOKESTATIC); // actual call
            out.writeShort(methodRef[i]);
            out.writeByte(ByteCode.INSTR_ARETURN);
        } // end for

        out.writeByte(ByteCode.INSTR_ACONST_NULL); // default case
        out.writeByte(ByteCode.INSTR_ARETURN);

        out.writeShort(0); // exception_table_length
        out.writeShort(0); // attributes_count

        out.writeShort(0); // attributes_count (for class)


        final byte[] res = baos.toByteArray();
        out.close();
        baos.close();
        return res;
    } // end method 'createBytecode(String, Method[])'

    /**
     * Parent class for all compiled dispatchers. <br/>
     * Handles every aspects but primitive dispatch (to be done by
     * implementing {@link fr.x9c.cadmium.kernel.DispatcherCompiler.AbstractCompiledDispatcher#call(int, fr.x9c.cadmium.kernel.CodeRunner, fr.x9c.cadmium.kernel.Value[])}
     */
    public abstract static class AbstractCompiledDispatcher
        implements Dispatcher {

        /**
         * Underlying dispatcher, keeping names and methods,
         * and allowing primitive addition.
         */
        private Dispatcher disp;

        /** Number of compiled primitives. */
        private int compiled;

        /**
         * {@inheritDoc}
         */
        @Override
        public final String[] getNames() {
            return this.disp.getNames();
        } // end method 'getNames()'

        /**
         * {@inheritDoc}
         */
        @Override
        public final Method[] getMethods() {
            return this.disp.getMethods();
        } // end method 'getMethods()'

        /**
         * {@inheritDoc}
         */
        @Override
        public final String getName(final int prim) {
            return this.disp.getName(prim);
        } // end method 'getName(int)'

        /**
         * {@inheritDoc}
         */
        @Override
        public final Method getMethod(final int prim) {
            return this.disp.getMethod(prim);
        } // end method 'getMethod(int)'

        /**
         * {@inheritDoc}
         */
        @Override
        public final int size() {
            return this.disp.size();
        } // end method 'size()'

        /**
         * {@inheritDoc}
         */
        @Override
        public final Value invoke(final int prim,
                                  final CodeRunner ctxt,
                                  final Value[] params)
            throws Fatal.Exception, Fail.Exception, FalseExit {
            if ((prim >= 0) && (prim < this.compiled)) {
                return call(prim, ctxt, params);
            } else {
                return this.disp.invoke(prim, ctxt, params);
            } // end if/else
        } // end method 'invoke(int, CodeRunner, Value[])'

        /**
         * {@inheritDoc}
         */
        @Override
        public final int addPrimitive(final String name, final Method impl) {
            return this.disp.addPrimitive(name, impl);
        } // end method 'addPrimitive(String, Method)'

        /**
         * Returns the number of primitives that are not compiled.
         * @return the number of primitives that are not compiled
         */
        final int notCompiled() {
            return this.disp.size() - this.compiled;
        } // end method 'notCompiled()'

        /**
         * Actual call of compiled primitive.
         * @param prim index of primitive to call
         * @param ctxt context
         * @param params parameters to primitive - should not be <tt>null</tt>
         * @return the result of the primitive,
         *         undefined if the primitive does not exist
         * @throws Fail.Exception if thrown by the primitive
         * @throws Fatal.Exception if thrown by the primitive
         */
        protected abstract Value call(final int prim,
                                      final CodeRunner ctxt,
                                      final Value[] params)
            throws Fail.Exception, Fatal.Exception, FalseExit;

    } // end inner-class 'AbstractCompiledDispatcher'

} // end class 'DispatcherCompiler'
